{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "36737349-c949-4d64-9aa3-3767cbd02ad1",
      "metadata": {
        "id": "36737349-c949-4d64-9aa3-3767cbd02ad1"
      },
      "source": [
        "# Map-reduce\n",
        "\n",
        "## Review\n",
        "\n",
        "We're building up to a multi-agent research assistant that ties together all of the modules from this course.\n",
        "\n",
        "To build this multi-agent assistant, we've been introducing a few LangGraph controllability topics.\n",
        "\n",
        "We just covered parallelization and sub-graphs.\n",
        "\n",
        "## Goals\n",
        "\n",
        "Now, we're going to cover [map reduce](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "f24e95c8",
      "metadata": {
        "id": "f24e95c8"
      },
      "outputs": [],
      "source": [
        "%%capture --no-stderr\n",
        "%pip install -U langchain_google_genai langgraph langchain"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "ff57cbf7",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ff57cbf7",
        "outputId": "68649737-359a-4977-b0e0-3e1a185df5f7"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "env: GOOGLE_API_KEY=AIzaSyBNVdQUtPyTc4AVRMyTT7JsvZocFHZvePE\n",
            "env: LANGCHAIN_API_KEY=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\n"
          ]
        }
      ],
      "source": [
        "import os\n",
        "from google.colab import userdata\n",
        "\n",
        "%env GOOGLE_API_KEY = {userdata.get('GEMINI_API_KEY')}\n",
        "\n",
        "%env LANGCHAIN_API_KEY = {userdata.get('LANGCHAIN_API_KEY')}\n",
        "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
        "os.environ[\"LANGCHAIN_PROJECT\"] = \"langchain-academy\""
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cbcd868a",
      "metadata": {
        "id": "cbcd868a"
      },
      "source": [
        "We'll use [LangSmith](https://docs.smith.langchain.com/) for [tracing](https://docs.smith.langchain.com/concepts/tracing)."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2bbe9b9f-4375-4bca-8e32-7d57cb861469",
      "metadata": {
        "id": "2bbe9b9f-4375-4bca-8e32-7d57cb861469"
      },
      "source": [
        "## Problem\n",
        "\n",
        "Map-reduce operations are essential for efficient task decomposition and parallel processing.\n",
        "\n",
        "It has two phases:\n",
        "\n",
        "(1) `Map` - Break a task into smaller sub-tasks, processing each sub-task in parallel.\n",
        "\n",
        "(2) `Reduce` - Aggregate the results across all of the completed, parallelized sub-tasks.\n",
        "\n",
        "Let's design a system that will do two things:\n",
        "\n",
        "(1) `Map` - Create a set of jokes about a topic.\n",
        "\n",
        "(2) `Reduce` - Pick the best joke from the list.\n",
        "\n",
        "We'll use an LLM to do the job generation and selection."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "994cf903-1ed6-4ae2-b32a-7891a2808f81",
      "metadata": {
        "id": "994cf903-1ed6-4ae2-b32a-7891a2808f81"
      },
      "outputs": [],
      "source": [
        "# Prompts we will use\n",
        "subjects_prompt: str = \"\"\"\n",
        "You are a smart assistant that generates sub-topics for a given topic. You must return the sub-topics in valid JSON format, with a key `\"subjects\"` and the value as a list of three related sub-topics.\n",
        "\n",
        "Each sub-topic should be a short, clear, and related concept or idea. The output should be formatted like this:\n",
        "{{\n",
        "  \"subjects\": [\"sub-topic1\", \"sub-topic2\", \"sub-topic3\"]\n",
        "}}\n",
        "\n",
        "Here are some examples:\n",
        "\n",
        "Example 1:\n",
        "Topic: \"Technology\"\n",
        "Response: {{\n",
        "  \"subjects\": [\"Artificial Intelligence\", \"Blockchain\", \"Quantum Computing\"]\n",
        "}}\n",
        "\n",
        "Example 2:\n",
        "Topic: \"Sports\"\n",
        "Response: {{\n",
        "  \"subjects\": [\"Soccer\", \"Basketball\", \"Tennis\"]\n",
        "}}\n",
        "\n",
        "Example 3:\n",
        "Topic: \"Music\"\n",
        "Response: {{\n",
        "  \"subjects\": [\"Jazz\", \"Classical\", \"Pop\"]\n",
        "}}\n",
        "\n",
        "Now, generate a list of 3 sub-topics that are related to this overall topic:\n",
        "Topic: \"{topic}\"\n",
        "\n",
        "Return the output as valid JSON, exactly like the examples above, with the key `\"subjects\"` and the value as a list of strings.\n",
        "\"\"\"\n",
        "\n",
        "\n",
        "\n",
        "joke_prompt: str = \"\"\"Generate a joke about {subject}. Make it crunchy and enjoyable\"\"\"\n",
        "\n",
        "best_joke_prompt: str = \"\"\"Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one, starting 0 as the ID for the first joke. Jokes: \\n\\n  {jokes}\n",
        "\n",
        "Return in JSON format. The output should be formatted like this:\n",
        "{{\n",
        "  \"id\": 0\n",
        "}}\n",
        "\"\"\""
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# .format() is a string method in Python. It allows you to insert values into a string by using placeholders (curly braces {}) and specifying the values you want\n",
        "# to substitute for those placeholders. i.e:\n",
        "\n",
        "subjects_prompt.format(topic=\"animals\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 140
        },
        "id": "wzskjuFN99GR",
        "outputId": "05c7edba-9dbf-472e-d2a3-3d08a0b93bcb"
      },
      "id": "wzskjuFN99GR",
      "execution_count": 4,
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "'\\nYou are a smart assistant that generates sub-topics for a given topic. You must return the sub-topics in valid JSON format, with a key `\"subjects\"` and the value as a list of three related sub-topics.\\n\\nEach sub-topic should be a short, clear, and related concept or idea. The output should be formatted like this:\\n{\\n  \"subjects\": [\"sub-topic1\", \"sub-topic2\", \"sub-topic3\"]\\n}\\n\\nHere are some examples:\\n\\nExample 1:\\nTopic: \"Technology\"\\nResponse: {\\n  \"subjects\": [\"Artificial Intelligence\", \"Blockchain\", \"Quantum Computing\"]\\n}\\n\\nExample 2:\\nTopic: \"Sports\"\\nResponse: {\\n  \"subjects\": [\"Soccer\", \"Basketball\", \"Tennis\"]\\n}\\n\\nExample 3:\\nTopic: \"Music\"\\nResponse: {\\n  \"subjects\": [\"Jazz\", \"Classical\", \"Pop\"]\\n}\\n\\nNow, generate a list of 3 sub-topics that are related to this overall topic:\\nTopic: \"animals\"\\n\\nReturn the output as valid JSON, exactly like the examples above, with the key `\"subjects\"` and the value as a list of strings.\\n'"
            ],
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "string"
            }
          },
          "metadata": {},
          "execution_count": 4
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "from langchain_google_genai import ChatGoogleGenerativeAI\n",
        "\n",
        "# LLM\n",
        "model: ChatGoogleGenerativeAI = ChatGoogleGenerativeAI(model = \"gemini-1.5-flash\")"
      ],
      "metadata": {
        "id": "0LgocmSx6rmo"
      },
      "id": "0LgocmSx6rmo",
      "execution_count": 5,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "f3b883cc-3469-4e96-b1a4-deadf7bf3ce5",
      "metadata": {
        "id": "f3b883cc-3469-4e96-b1a4-deadf7bf3ce5"
      },
      "source": [
        "## State\n",
        "\n",
        "### Parallelizing joke generation\n",
        "\n",
        "First, let's define the entry point of the graph that will:\n",
        "\n",
        "* Take a user input topic\n",
        "* Produce a list of joke topics from it\n",
        "* Send each joke topic to our above joke generation node\n",
        "\n",
        "Our state has a `jokes` key, which will accumulate jokes from parallelized joke generation"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "099218ca-ee78-4291-95a1-87ee61382e3b",
      "metadata": {
        "id": "099218ca-ee78-4291-95a1-87ee61382e3b"
      },
      "outputs": [],
      "source": [
        "import operator\n",
        "from typing import Annotated\n",
        "from typing_extensions import TypedDict\n",
        "from pydantic import BaseModel\n",
        "\n",
        "class Subjects(BaseModel):\n",
        "    subjects: list\n",
        "\n",
        "class BestJoke(BaseModel):\n",
        "    id: int\n",
        "\n",
        "class OverallState(TypedDict):\n",
        "    topic: str\n",
        "    subjects: list\n",
        "    jokes: Annotated[list, operator.add]\n",
        "    best_selected_joke: str"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "response = model.invoke(subjects_prompt.format(topic=\"animals\"))\n",
        "\n",
        "print(\"\\nresponse.content\\n\", response.content)\n",
        "# Use a Parser to parse LLM response and get subjects\n",
        "from langchain.output_parsers import PydanticOutputParser\n",
        "\n",
        "# Initialize PydanticOutputParser\n",
        "parser = PydanticOutputParser(pydantic_object=Subjects)\n",
        "\n",
        "# Parse the response generated above\n",
        "parsed_response = parser.parse(response.content)\n",
        "\n",
        "# Print the parsed structured data\n",
        "print(parsed_response.subjects)\n",
        "print(type(parsed_response))\n",
        "print(type(parsed_response.subjects))"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Ka3niFAPc7MC",
        "outputId": "35ee5e9f-86ac-480a-e789-6ac17d1df632"
      },
      "id": "Ka3niFAPc7MC",
      "execution_count": 9,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "response.content\n",
            " ```json\n",
            "{\n",
            "  \"subjects\": [\"Mammals\", \"Birds\", \"Insects\"]\n",
            "}\n",
            "```\n",
            "['Mammals', 'Birds', 'Insects']\n",
            "<class '__main__.Subjects'>\n",
            "<class 'list'>\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c7176d1c-4a88-4b0f-a960-ee04a45279bd",
      "metadata": {
        "id": "c7176d1c-4a88-4b0f-a960-ee04a45279bd"
      },
      "source": [
        "Generate subjects for jokes."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "45010efd-ad31-4daa-b77e-aaec79ef0309",
      "metadata": {
        "id": "45010efd-ad31-4daa-b77e-aaec79ef0309"
      },
      "outputs": [],
      "source": [
        "def generate_topics(state: OverallState) -> Subjects:\n",
        "    print(\"generate_topics_state\", state)\n",
        "    prompt = subjects_prompt.format(topic=state[\"topic\"])\n",
        "\n",
        "    response = model.invoke(prompt)\n",
        "    # Parse the response\n",
        "    parsed_response = PydanticOutputParser(pydantic_object=Subjects).parse(response.content)\n",
        "\n",
        "    return {\"subjects\": parsed_response.subjects}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e5296bb0-c163-4e5c-8181-1e305b37442a",
      "metadata": {
        "id": "e5296bb0-c163-4e5c-8181-1e305b37442a"
      },
      "source": [
        "Here is the magic: we use the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send) to create a joke for each subject.\n",
        "\n",
        "This is very useful! It can automatically parallelize joke generation for any number of subjects.\n",
        "\n",
        "* `generate_joke`: the name of the node in the graph\n",
        "* `{\"subject\": s`}: the state to send\n",
        "\n",
        "`Send` allow you to pass any state that you want to `generate_joke`! It does not have to align with `OverallState`.\n",
        "\n",
        "In this case, `generate_joke` is using its own internal state, and we can popular this via `Send`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "bc83e575-11f6-41a9-990a-adb571bcda06",
      "metadata": {
        "id": "bc83e575-11f6-41a9-990a-adb571bcda06"
      },
      "outputs": [],
      "source": [
        "from langgraph.constants import Send\n",
        "def continue_to_jokes(state: OverallState):\n",
        "    return [Send(\"generate_joke\", {\"subject\": s}) for s in state[\"subjects\"]]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9847192d-d358-411e-90c0-f06be0738717",
      "metadata": {
        "id": "9847192d-d358-411e-90c0-f06be0738717"
      },
      "source": [
        "### Joke generation (map)\n",
        "\n",
        "Now, we just define a node that will create our jokes, `generate_joke`!\n",
        "\n",
        "We write them back out to `jokes` in `OverallState`!\n",
        "\n",
        "This key has a reducer that will combine lists."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "bcddc567-73d3-4fb3-bfc5-1bea538f2aab",
      "metadata": {
        "id": "bcddc567-73d3-4fb3-bfc5-1bea538f2aab"
      },
      "outputs": [],
      "source": [
        "class JokeState(TypedDict):\n",
        "    subject: str\n",
        "\n",
        "class Joke(BaseModel):\n",
        "    joke: str\n",
        "\n",
        "def generate_joke(state: JokeState) -> list[Joke]:\n",
        "    prompt = joke_prompt.format(subject=state[\"subject\"])\n",
        "    response = model.with_structured_output(Joke).invoke(prompt)\n",
        "    return {\"jokes\": [response.joke]}"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "res = generate_joke(state={\"subject\": \"GANs\"})\n",
        "print(res)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "e_sF7qM8_DZu",
        "outputId": "74fe9b3f-8214-42bf-bbe4-5f78dda1d0dd"
      },
      "id": "e_sF7qM8_DZu",
      "execution_count": 14,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "{'jokes': ['Why did the GAN cross the road? To generate a better image on the other side!']}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "id": "02960657-d174-4076-99a8-b3f9eea015f4",
      "metadata": {
        "id": "02960657-d174-4076-99a8-b3f9eea015f4"
      },
      "source": [
        "### Best joke selection (reduce)\n",
        "\n",
        "Now, we add logic to pick the best joke."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "8d672870-75e3-4307-bda0-c41a86cbbaff",
      "metadata": {
        "id": "8d672870-75e3-4307-bda0-c41a86cbbaff"
      },
      "outputs": [],
      "source": [
        "def best_joke(state: OverallState):\n",
        "    print(\"best_joke_OverallState\", state)\n",
        "    jokes = \"\\n\\n\".join(state[\"jokes\"])\n",
        "    prompt = best_joke_prompt.format(topic=state[\"topic\"], jokes=jokes)\n",
        "    # response = model.with_structured_output(BestJoke).invoke(prompt)\n",
        "    response = model.invoke(prompt)\n",
        "    # print(response.content)\n",
        "    parsed_response = PydanticOutputParser(pydantic_object=BestJoke).parse(response.content)\n",
        "    # print(parsed_response)\n",
        "    return {\"best_selected_joke\": state[\"jokes\"][parsed_response.id]}"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "overallState = {'topic': 'playing', 'subjects': ['Generative Adversarial Networks (GANs)', 'Diffusion Models', 'Prompt Engineering'], 'jokes': ['Why did the prompt engineer get lost in the woods? Because he kept asking for directions to the \"nearest forest.\"', 'Why did the prompt engineer get in the woods? Because he kept asking for directions to the \" forest.\"', 'Why did the prompt engineer get lost? Because he kept asking for directions\"']}\n",
        "res =  best_joke(state=overallState)\n",
        "print(\"\\nres\\n\", res)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "eLBvilZWnLJc",
        "outputId": "ec218801-37a0-4d57-ffa7-7b597303bb06"
      },
      "id": "eLBvilZWnLJc",
      "execution_count": 16,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "best_joke_OverallState {'topic': 'playing', 'subjects': ['Generative Adversarial Networks (GANs)', 'Diffusion Models', 'Prompt Engineering'], 'jokes': ['Why did the prompt engineer get lost in the woods? Because he kept asking for directions to the \"nearest forest.\"', 'Why did the prompt engineer get in the woods? Because he kept asking for directions to the \" forest.\"', 'Why did the prompt engineer get lost? Because he kept asking for directions\"']}\n",
            "\n",
            "res\n",
            " {'best_selected_joke': 'Why did the prompt engineer get lost in the woods? Because he kept asking for directions to the \"nearest forest.\"'}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "id": "837cd12e-5bff-426e-97f4-c774df998cfb",
      "metadata": {
        "id": "837cd12e-5bff-426e-97f4-c774df998cfb"
      },
      "source": [
        "## Compile"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "2ae6be4b-144e-483c-88ad-ce86d6477a0d",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 449
        },
        "id": "2ae6be4b-144e-483c-88ad-ce86d6477a0d",
        "outputId": "c6a35ee3-da43-439f-ba3b-38789b98f061"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAGwAKEDASIAAhEBAxEB/8QAHQABAQADAQEBAQEAAAAAAAAAAAYEBQcIAgMBCf/EAFYQAAEDAwEDBQoICgUJCQAAAAEAAgMEBQYRBxIhExUxQZQIFBYXIlFVVtHTMjZUYXF0k9IjJHWBlaGys7TUNUJzkbEJGDM0UmJygpIlOENEU1eFovD/xAAaAQEAAwEBAQAAAAAAAAAAAAAAAQIEAwUG/8QANhEBAAEBBAUKBQQDAQAAAAAAAAERAgNRkQQSFCExEzNBUmJxkqGx0QVhgcHhFSIj8DJCU7L/2gAMAwEAAhEDEQA/AP8AVNERAREQEWDebvDZKB9TM2STiGRwwjekmeeDWMHW4nh1DrJABK0oxWbIW8vkcr52vHC0wyEUsQ16HaaGV3US7yfM0dfWzYiY1rU0j+8E0bioyC10shZPcqOF46WyTsaR+Ylfn4VWX0xQdpZ7V+cGG2ClZuQ2O2xM/wBllJG0fqC/TwVsvoeg7Mz2K/8AD8/JO48KrL6YoO0s9qeFVl9MUHaWe1PBWy+h6DszPYngrZfQ9B2ZnsT+H5+RuPCqy+mKDtLPanhVZfTFB2lntTwVsvoeg7Mz2J4K2X0PQdmZ7E/h+fkbjwqsvpig7Sz2r6jyW0TPDY7rRPcehrahhJ/WvnwVsvoeg7Mz2L4kxGxTMLJLLbnsPS11JGQf1J/D8/JG5tgQ4Ag6g8QQv6pl2Ew2smfHpTZJwS7kIhrSSk9T4egD52brvn6QdlYb1zvDKyeA0VwpncnVUjnb3Ju6i12g3mOHFrtBqOkNILRW1YimtYmseZTBtERFxQIiICIiAiIgIiIJh+l32gthfo6GzUbKljTr/p5zIwO82rY45B9EpVOpihb3ntEuzHajv63080R04ExPkbINfm5SL/qVOtF9xsxHCkfnzqmRERZ0Ob0HdC4Jeam+U1qu810qbPT1NTUNpLfVSMe2A7svJPERbMWuIaREXHUgaLSbN+6cxnM9kFPnl1bV2GmbDA6thlt9W5sMkp0ZHE4wg1GpIAdEHAkjzqI2VwXqz7U58fw+yZZaNn1VDcZrnbcotxgpLbVOkDo3UEx4vZK98jjG1z2gHeG6ToJ7GLzm9k7mjF8Qt+O5hj93x2egtmRy0lqf313kHvZUPt7iCJ3aMad6PeIa/UcegO8UvdA4BWYHc8yjyFgx21ztpq+pkppmSUkrnMaGSwuYJGHWRnwmjg4Ho4qQyrutMUsN6w+mo6e63K3XyuqKSWtjs1frEyKmdMHxMFOTOHExgFmo3XOcCQ06cPuuC3i44Rt1o6DFcymor7VWCrtjMggqKqsromSwxzOJeXvJHJOJY8h7WbpLWjo73t+p7jasq2W5bSWW5Xy3Y7ep5LhTWeldU1McU1FPA2RsTfKeGve3XdBIB10Qdjp52VVPFNHvcnI0PbvtLToRqNQQCD8x4r9Fi2uvbdbZR1rYJ6ZtTCyYQ1URiljDmg7r2Hi1w10IPEHULKQFMX7S05bYbjHo3v17rZU9Or2lj5Iif+FzXAf2rvOqdTGXN78vGL0LdS91wNU7Qa6RxRPJJ83lOjH/ADBaLj/OnRSfSUwp0RFnQIiICIiAiIgIiINNkdmmuDaWsoXRx3WgeZaV0pIY/Vpa6N5Gp3HA6E6HQhrtCWgL84a+1Zpba+1VtMyTlYXU9ws9expeGPBa5kjOIcxw1Go1a4dBIW9WrvWM23IBGa6m35Y9RHPG90U0evTuSMIc3q6COhdrNqzMatvo6U96LHc2bJ2kEbN8WBHQRaYAR/8AVfUPc47KqaaOWLZzi8csbg5j22mAFpHEEHdW+8B3s1EOSX6Fn+z302TT872OP608Caj1qv320Pulbk7vr+UlIxVCKX8Caj1qv320Puk8Caj1qv320Puk5O76/lJSMVQi5Zsmt12zPZdiF/uWU3gXG6WmlrakU8sIj5SSJr3bv4M+TqTpxPDrVX4E1HrVfvtofdJyd31/KSkYtXetguzfI7rVXO64HjtxuNU8yT1dVbIZJZXHpc5xbqT85WF/m17J/wD23xY//EQfdVD4E1HrVfvtofdJ4DzOGj8nvz29Y74jb+tsYP605O76/lJSMWTS0+O7NcepbfQ0tJZLVCXMpbfQwBgLiS4siiYNXOJJO60Ekk8F/bFbame4T3u5RchWzxiGClLg40sAOoYSCQZHHynlvDUNaC4MDnfraMRtllqTVQwvmrSCDWVcz6ifQ9ID3kkA8PJGg4DhwC3KibVmzE2bHT0ncIiLggREQEREBERAREQEREBERAREQc+7nstdsG2dFhJaceoCCRoSO92fOf8AE/SV0Fc/7nvXxD7OtS0nweoNSwNDf9XZ0bvDT6OC6AgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIOe9zwANgezgBzXgY7b/KYNAfxdnEDQcPzLoS573O+niC2cbpJb4O2/Qlu7/5dnUOj6F0JAREQEREBERAREQEREBF/HODGlziGtA1JJ4AKKOYXu7AVFltlCba/jDUXCpfHJM3qeI2xndaekanUjpAXa7urV7XV9k0qtkURz7mHyCx9rm92nPuYfILH2ub3a7bLbxjOCi3RRHPuYfILH2ub3ac+5h8gsfa5vdpstvGM4KLdFEc+5h8gsfa5vdpz7mHyCx9rm92my28Yzgot1yfumNuc/c8bNxl0WNS5PAytipqiniqu9+Qje1/4Uu3H8A5rG6aD4Y48ONDz7mHyCx9rm92tDntnvu0bDLzjF5tVjmtt0pn00ze+pSQHDg4axfCadHD5wE2W3jGcFHL+4S7oOu2zYNDZYsQdZrPilupba67PrhIKqdsYaGtiETQPJaXHR3k6tGnHUepVwfYPs0vOwLZtbsQs1JZqmKnc+aeslqJWyVMzzq6RwEegOmjQOoNA6l0Hn3MPkFj7XN7tNlt4xnBRboojn3MPkFj7XN7tOfcw+QWPtc3u02W3jGcFFuiiOfcw+QWPtc3u059zD5BY+1ze7TZbeMZwUW6KI59zD5BY+1ze7Tn3MPkFj7XN7tNlt4xnBRboopuS5RSHlaqz26qp28Xx0VY/lt3r3A+MNcfmLm6+dVluuNPdqCnraSQTU07BJG8AjVpGo4HiPoPELleXNu7is8MylGSiIuCGrygluM3cg6EUcxBH/AVPYyAMbtQAAApItAP+AKhyr4sXj6nN+wVPY18XLV9Ui/YC9G55me/7J6GyREVkCIiAiIgIiwbZfLfen1raCtgrXUVQ6kqRBIH8jM0NLo3adDgHN1HSNUGciIgIsG03y336Gaa21sFfDDPJTSSU8ge1ssbi2RhI/rNcCCOoghZyAiLU47lVryuOvktdSallDWzW+oJiezcniduyM8oDXQ9Y1B6iVA2yIikFj7LjrhFB80k4HzATyaLIWPst+JFD/a1H7+RRe8xPfHpaT0KtEReahq8q+LF4+pzfsFT2NfFy1fVIv2AqHKvixePqc37BU9jXxctX1SL9gL0bnmZ7/snoZla6dlHO6lY2SpEbjEx50a5+nAE+bXReOdl2SX695DgN4pb9l+Q3+mhuVXmdorZ6llFSVEdPK1kfJ6COMichjI26hw8oglocPZbtSDppr86827OO52y/Ec3sVeyptGNWi2VDpJ47FerrUtr4dxzW05pamQwws1c13klxG4N3RVtRNYQm9j9Htbzy14XntJdQ/nSogrrhNPlcs1JNSuf+Hgbbu9BHE5rd5rd1+81zRq93HX8WXHILds2uO0IZfkU92tmdSUMVJNcpHUbqM3nvY07ofgvbuSHRzgXN0aGuDWgDvtk2B4FjmVNyK2WBtFc2Tvqo+SqpxTxzPBa+RlPv8kxxDnAlrAeJWxk2SYnLi1VjjrVrZqq4G6zU3fMvlVRqRUmTe394fhgHboO71aacFGrI88TO2o7X8m2h1uP101DPZL5VWa2GPKpbfDQ8gGiN8tE2kkZOH6iQmRx3g/dG6ArXELPfcz285nTZDkt4ggsdBYqjmq03OaCjNW+OV0rgGkEsLoyCzg14d5QJDdOgZLsCwLLsmlyC6WBst1n3BUSw1U8Danc+By0cb2sl00AG+HcAAqm3YjabTkl4v8AS0nJXa7sgjrajlHnlWwhwiG6Tut3Q93wQNdeOvBTFmekeW7dmWQnPcKzSwVmR+B+R5U60iS+5AaiKtgk5cfg6Dk92BjXR6seHh+jBvNO9qtjs8pDszxLb9mdpqrtX3ay3i9CnpKy51E9M4sghla98Lnlrn6gavI3t3hrouwxdzhs6gujLhHjgZVRVguFOW1lQGUtQJBJvwM5TdhJeNSIw0O4gggkLd0+yTE6TNK7K4LVyN7rmltVKyolENRqzcLnwb/JOcW8N4t1+dRFmRxfZDjG1KS7YbkxuhqLPXRtqbtLW5ZLcoq+CWEuDoaY0kbIHB5Y5vJuDQAWkEHVelnt32lpJAI01B0K5zj+wXEcBqqm54faILPehBNHRSVE1RUUtK5/E7lOZQ1jCdNWx7moGmoWRS2vak2qhNTk2ISU4eDIyLHapj3N14hrjXEA6dBIP0FWisDz5hffOyHudNq2XY9cbrNeqK63mlhFfcp6qCDdr3sE/JSOczlGg77n6bztCXE6lUOdNu+xq/W+12nNMhv0GQYve5as3a5PqnxTU1K2WKshcTrCS5xbowhnlN0AI1XbKLYzhtuv99vNPZWMq74yWO5RmeV1NVCXTlS6nLjFq/dG84M1PWTqVoR3PGJY5jeTU+KWiKgu90tE9qiq6yrnqORjexwbG10jnmOIOIJYwAcBwOgVdWRzGwOu2EQbC7zS5bkN3rMvNNQ3e23a6SVcdRHLQPmfOxjyeSdG9jTqzTUO466rR45kGQ1RseADLL5R26657frVPe5bhJNXtpKPlHQ0zKiQuc0v3Gt3td7QHTiV2fZF3O+K7MaOwV4tUUmVUNsiopa81c9QyN/JtbLyDZXERNcQfgNZqDxHUt9ctieE3jHrhY62wxVNtrrnLeZo3zS7wrZHl752Sb2/G7eJILC3TUgaDgmrI59tQ2Z5VYMYsdNi97y2/wBnorjJV3egivz47vU05iLWsgqnEOIY/R/JueC/Uje6AunbKsiteV7OMdutluFbdLZUUcZhrLkSaqUAbpMx0H4TUEO+cFaGbudMAnsUFofZqjvOGqfWsc26VbZ+WewMe8zCXlHEta1p1cRoAFc49j1txOx0Nns9FFb7XRRNgp6WBujI2DoA9vWrRExI2Cx9lvxIof7Wo/fyLIWPst+JFD/a1H7+RWveYnvj0tJ6FWiIvNQ1eVfFi8fU5v2Cp7Gvi5avqkX7AVjUQR1UEkMrd+KRpY5p6wRoQoOGlv8AjNPDbm2Sa+U9OxsUNZR1ELXPYBo3lGyvZo/QcdCQenhruj0NHmJsTYrSa13zT1WjfFG9RaTna/epl17VRe/Tna/epl17VRe/XfU7UeKPco3aLSc7X71MuvaqL36c7X71MuvaqL36anajxR7lG7RaTna/epl17VRe/Tna/epl17VRe/TU7UeKPco3aLSc7X71MuvaqL36c7X71MuvaqL36anajxR7lG7RTFmyy7X60UVzo8NvJpKyFlRCZpKWJ5Y5oc3eY+YOadD8FwBHQQFmc7X71MuvaqL36anajxR7lG7RaTna/epl17VRe/Tna/epl17VRe/TU7UeKPco3aLSc7X71MuvaqL36c7X71MuvaqL36anajxR7lG7RaTna/epl17VRe/Tna/epl17VRe/TU7UeKPco3ax9lvxIof7Wo/fyLXNq8krTyUGMT0EjuAqLhVQGJn+8RFI9ztPMANejUdIq8es0ePWWjt0T3Stp2bpkf8ACe7pc4/OSSfzrjfzFm61KxWZid0xPCuHecIbFERecqIiICIiAiIgIiICIiCf2fTiqwTHZhPcaoSW+nfy94j5Otk1jad6dug3ZD0uGg0OqoFObN6gVez3GZ21dfcGyW2neKu6s3KubWNp35m9Uh6XDqJKo0BERAREQEREBERAREQEREBERAREQEREBERBObN6xlx2e4zVR19ZdI57bTyNrrjHydTUAxtIklb/AFXu6SOokqjU7s6recsAxur79rLly9up5e/LhCIaifWNp35WDQNeekt6iSFRICIiAiIgIiICIiAiIgIiICKan2l4lTSujlya0Me0kFprY9QegjpX5+NLDvWm0dtj9q0bPfT/AKTlKaTgqUUt40sO9abR22P2p40sO9abR22P2ps991JylOrOCpRS3jSw71ptHbY/anjSw71ptHbY/amz33UnKTVnBUrVZHldkw+ijrL9eLfZKSSQQsqLjVMp43PIJDA55ALtGuOnToD5lq/Glh3rTaO2x+1c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\n",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {},
          "execution_count": 20
        }
      ],
      "source": [
        "from IPython.display import Image\n",
        "from langgraph.graph import END, StateGraph, START\n",
        "from langgraph.graph.state import CompiledStateGraph\n",
        "\n",
        "# Construct the graph: here we put everything together to construct our graph\n",
        "graph: StateGraph = StateGraph(OverallState)\n",
        "graph.add_node(\"generate_topics\", generate_topics)\n",
        "graph.add_node(\"generate_joke\", generate_joke)\n",
        "graph.add_node(\"best_joke\", best_joke)\n",
        "graph.add_edge(START, \"generate_topics\")\n",
        "graph.add_conditional_edges(\"generate_topics\", continue_to_jokes, [\"generate_joke\"])\n",
        "graph.add_edge(\"generate_joke\", \"best_joke\")\n",
        "graph.add_edge(\"best_joke\", END)\n",
        "\n",
        "# Compile the graph\n",
        "app: CompiledStateGraph = graph.compile()\n",
        "Image(app.get_graph().draw_mermaid_png())"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "e21dc7c9-0add-4125-be76-af701adb874a",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "e21dc7c9-0add-4125-be76-af701adb874a",
        "outputId": "7b0cafbd-8393-4a55-b001-174cb86233ab"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "generate_topics_state {'topic': 'online classes', 'jokes': []}\n",
            "{'generate_topics': {'subjects': ['Learning Management Systems (LMS)', 'Virtual Classroom Environments', 'Online Assessment Tools']}}\n",
            "{'generate_joke': {'jokes': ['Why did the LMS get a bad grade? Because it couldn’t pass the students!']}}\n",
            "{'generate_joke': {'jokes': ['Why did the virtual classroom get a bad grade? Because it couldn’t pass the unmute test!']}}\n",
            "{'generate_joke': {'jokes': ['Why did the online assessment tool get fired? Because it was always proctoring!']}}\n",
            "best_joke_OverallState {'topic': 'online classes', 'subjects': ['Learning Management Systems (LMS)', 'Virtual Classroom Environments', 'Online Assessment Tools'], 'jokes': ['Why did the LMS get a bad grade? Because it couldn’t pass the students!', 'Why did the virtual classroom get a bad grade? Because it couldn’t pass the unmute test!', 'Why did the online assessment tool get fired? Because it was always proctoring!']}\n",
            "{'best_joke': {'best_selected_joke': 'Why did the online assessment tool get fired? Because it was always proctoring!'}}\n"
          ]
        }
      ],
      "source": [
        "# Call the graph: here we call it to generate a list of jokes\n",
        "for s in app.stream({\"topic\": \"online classes\"}):\n",
        "    print(s)"
      ]
    },
    {
      "cell_type": "markdown",
      "source": [],
      "metadata": {
        "id": "5A4GBUBhDqr5"
      },
      "id": "5A4GBUBhDqr5"
    },
    {
      "cell_type": "markdown",
      "id": "2a96517e-77ab-46e2-95e2-79168c044e9c",
      "metadata": {
        "id": "2a96517e-77ab-46e2-95e2-79168c044e9c"
      },
      "source": [
        "## Studio\n",
        "\n",
        "--\n",
        "\n",
        "**⚠️ DISCLAIMER**\n",
        "\n",
        "*Running Studio currently requires a Mac. If you are not using a Mac, then skip this step.*\n",
        "\n",
        "*Also, if you are running this notebook in CoLab, then skip this step.*\n",
        "\n",
        "--\n",
        "\n",
        "Let's load our the above graph in the Studio UI, which uses `module-4/studio/map_reduce.py` set in `module-4/studio/langgraph.json`.\n",
        "\n",
        "![Screenshot 2024-08-28 at 3.17.53 PM.png](https://cdn.prod.website-files.com/65b8cd72835ceeacd4449a53/66dbb0c0ed88a12e822811e2_map-reduce1.png)"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.12.5"
    },
    "colab": {
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}